﻿// (c) Copyright Damian Mehers http://damianblog.com/
// This source is subject to the Microsoft Public License.
// See http://www.microsoft.com/resources/sharedsource/licensingbasics/sharedsourcelicenses.mspx.
// All other rights reserved.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Threading;
using MceFM.Last.FM;
using MceFM.Last.Inject;
using MceFM.Last.KeyHook;
using MceFM.Last.MediaPlayer;

namespace MceFM.Last {
  /// <summary>
  /// This class acts as an intermediary to Last.fm's servers.  It acts as an HTTP server, and adds mp3 tags to the streams that Last.fm returns.
  /// </summary>
  public class Server {
    public const string ARTIST_QUERY_STRING = "arst";
    public const string MCE_PAGE_CHANGED_ACTION = "McePageChanged";
    public const string PAGE_QUERY_STRING = "page";
    public const string PASSWORD_HASH_QUERY_STRING = "passwordHash";

    public const string PLAY_ARTIST_ACTION = "PlayArtist";
    public const string PLAY_TAG_ACTION = "PlayTag";
    public const string TAG_QUERY_STRING = "tag";
    public const string USERNAME_QUERY_STRING = "username";
    private const string MCE_AUDIO_NOW_PLAYING_PAGE = "MediaCenter.Audio.AudioNowPlayingPage";


    // Services that this class uses
    private readonly Downloader downloader;
    private readonly Hooker hooker;
    private readonly Injecter injecter;
    private readonly Scrobbler scrobbler;
    private HttpListener httpListener;
    readonly UserNotifier userNotifier = new UserNotifier();
    readonly Tracer tracer = new Tracer();
    private readonly MediaPlayerInfo mediaPlayer;

    private readonly string queueUrl;
    private readonly List<Track> tracks = new List<Track>();
    private readonly Dictionary<string, Track> cachedTracks = new Dictionary<string, Track>();

    private string currentMcePage;
    private readonly string listenUrl;



    // Credentials used to initialize previously.  Saved to check if credentials have changed (so that we know we need to re-initialized)
    private string initializedPasswordHash = "";
    private string initializedUsername = "";
    private LastFm lastFm;

    public Server(string url, string username, string passwordHash) {
      Trace.Listeners.Add(tracer);
      Trace.AutoFlush = true;
      downloader = new Downloader(userNotifier, tracks);
      hooker = new Hooker();
      injecter =  injecter = new Injecter();
      listenUrl = url;
      queueUrl = listenUrl.Replace("+", "localhost");
      mediaPlayer = new MediaPlayerInfo();
      mediaPlayer.TrackStoppedOrStarted += TrackStoppedOrStartedHandler;
      scrobbler = new Scrobbler(username, passwordHash, userNotifier);
    }

    private void TrackStoppedOrStartedHandler(object sender, TrackStoppedOrStartedEventArgs e) {
      Trace.TraceInformation("Track Stopped or Started: " + e);
      Track track = FindTrack(e.Name);
      if(track != null && e.Started) {
        downloader.SkipTo(track);
      }
      scrobbler.TrackStoppedOrStartedHandler(track, e);
    }


    private void FetchMoreTracks() {
      List<Track> newTracks = lastFm.FetchMoreTracks();
      if(newTracks.Count == 0) {
        return;
      }

      foreach(Track track in newTracks) {
        if(!string.IsNullOrEmpty(track.Image)) {
          track.DownloadImage(new WebClient());
        }
        tracks.Add(track);
        downloader.AddedTrack();
        string playUrl = queueUrl + track.Title;
        mediaPlayer.QueueUrl(playUrl, tracks.Count == 1);
      }
      return;
    }


    private bool Play(LastFm.PlayType playType, string name) {
      lastFm.Play(playType, name);
      ReloadTracks();
      return tracks.Count > 0;
    }

    private void ReloadTracks() {
      lock(tracks) {
        Reset();
        FetchMoreTracks();
      }
    }

    private void Reset() {
      lock(tracks) {
        cachedTracks.Clear();
        tracks.Clear();
      }
    }


    public static string GetPasswordHash(string password) {
      return LastFm.GetPasswordHash(password);
    }


    public void Start() {
      mediaPlayer.Start();
      downloader.Start();
      injecter.Start();
      hooker.Start();
      hooker.ArrowKey += NowPlayingArrowKeyHandler;

      httpListener = new HttpListener();

      httpListener.Prefixes.Add(listenUrl);
      httpListener.Start();
      Thread thread = new Thread(ConnectionListener) {IsBackground = true};
      thread.Start();
    }


    private void ConnectionListener() {
      try {
        while(true) {
          HttpListenerContext context = httpListener.GetContext();
          try {
            if (!context.Request.RemoteEndPoint.Address.Equals(context.Request.LocalEndPoint.Address)) {
            //if(!context.Request.IsLocal) {
              Trace.TraceError("Request is not local.  It comes from {0} (local is {1})",
                               context.Request.RemoteEndPoint,
                               context.Request.LocalEndPoint);
              SendError(context, "Only local clients are accepted");
              continue;
            }

            if (context.Request.Url.AbsolutePath.EndsWith(MCE_PAGE_CHANGED_ACTION)) {
              HandleCurrentMcePageChanged(context);
              continue;
            }

            if (context.Request.Url.AbsolutePath.EndsWith(PLAY_ARTIST_ACTION)) {
              HandlePlay(LastFm.PlayType.Artist, ARTIST_QUERY_STRING, context);
              continue;
            }

            if(context.Request.Url.AbsolutePath.EndsWith(PLAY_TAG_ACTION)) {
              HandlePlay(LastFm.PlayType.Tag, TAG_QUERY_STRING, context);
              continue;
            }


            Track track = null;

            Trace.TraceInformation("Got new connection for {0}", context.Request.Url.AbsolutePath);
            if(cachedTracks.ContainsKey(context.Request.Url.AbsolutePath)) {
              track = cachedTracks[context.Request.Url.AbsolutePath];
            } else {
              try {
                // Look for the track title in the list of tracks to play
                string localPath = context.Request.Url.LocalPath;
                int lastSlash = localPath.LastIndexOf('/');
                string title = localPath.Substring(lastSlash + 1);
                Trace.TraceInformation("Got new connection to play unplayed track {0}", title);
                foreach(Track searchTrack in tracks) {
                  if(searchTrack.Title == title) {
                    track = searchTrack;
                    break;
                  }
                }
                if(track == null) {
                  SendError(context, HttpStatusCode.NotFound, "Can't find track.");
                  continue;
                }
                cachedTracks[context.Request.Url.AbsolutePath] = track;
              } catch(Exception ex) {
                Trace.TraceError("Error fetching another track: {0}" + ex);
              }
            }

            if(track == null) {
              Trace.TraceError("Unexpected error.  Track is null");
              continue;
            }

            if(tracks[tracks.Count - 1] == track) {
              FetchMoreTracks();
            }

            Trace.WriteLine(string.Format("Got new connection for track {0}", track.Title));
            ConnectionHandler connectionHandler = new ConnectionHandler(track, context);
            connectionHandler.Start();
          } finally {
            Trace.Flush();
          }
        }
      } catch(Exception ex) {
        Trace.TraceError("ConnectionListener had unexpected error: {0}", ex);
        userNotifier.Notify("ConnectionListener had unexpected error:" + ex);
      }
    }

    private void HandleCurrentMcePageChanged(HttpListenerContext context) {
      currentMcePage = context.Request.QueryString[PAGE_QUERY_STRING];
      Trace.TraceInformation("Server notified of page change {0}", currentMcePage);
      context.Response.StatusCode = (int)HttpStatusCode.OK;
      context.Response.Close();

    }

    // Called when a left or right arrow key is pressed, and user is in the "now playing" page in Media Center
    private void NowPlayingArrowKeyHandler(object sender, ArrowKeyEventArgs e) {
      Trace.TraceInformation("NowPlayingArrowKeyHandler: Left={0}  Current Page={1}", e.LeftArrow, currentMcePage);

      if(currentMcePage != MCE_AUDIO_NOW_PLAYING_PAGE) {
        Trace.TraceInformation("Left/Right arrow but currentPage is {0}", currentMcePage);
        return;
      }

      // Check that MediaCenter is foreground.
      if(!Util.IsMediaCenterForeground()) {
        Trace.TraceInformation("Left/Right arrow but Media Center is not in the foreground");
        return;
      }

      if(e.LeftArrow) {
        scrobbler.Hate();
        mediaPlayer.Next();
      } else if(e.RightArrow) {
        scrobbler.Love();
      }
    }


    private Track FindTrack(string trackName) {
      // First look in tracks to be played
      foreach (Track track in tracks) {
        if (track.Title == trackName) {
          return track;
        }
      }
      return null;
    }

    private void HandlePlay(LastFm.PlayType playType, string queryString, HttpListenerContext context) {
      if(!HandleInitialize(context)) {
        return;
      }
      string name = context.Request.QueryString[queryString];
      Trace.WriteLine("PlayTag received for " + queryString + ": " + name);

      if(name == null) {
        SendError(context, queryString + " is required");
      } else {
        try {
          bool ok = Play(playType, name);
          if(ok) {
            StreamWriter streamWriter = new StreamWriter(context.Response.OutputStream);
            foreach(Track track in tracks) {
              string playUrl = queueUrl + track.Title;
              streamWriter.WriteLine(playUrl);
            }
            streamWriter.Flush();
            context.Response.StatusCode = (int)HttpStatusCode.OK;
            context.Response.Close();
          } else {
            SendError(context, "No tracks could be found for " + name);
          }
          Trace.WriteLine("PlayTag completed for " + queryString + ": " + name);
        } catch(Exception ex) {
          Trace.TraceError("Exception Playing {0} : {1}", queryString, ex);
          SendError(context, "Error Playing " + queryString + ".  Error is " + ex.Message);
        }
      }
    }


    private bool HandleInitialize(HttpListenerContext context) {
      string username = context.Request.QueryString[USERNAME_QUERY_STRING];
      string passwordHash = context.Request.QueryString[PASSWORD_HASH_QUERY_STRING];

      if(lastFm != null && lastFm.IsInitialized() && username == initializedUsername && passwordHash == initializedPasswordHash) {
        return true;
      }


      Trace.WriteLine("Initialize received for username " + username);

      if(username == null || passwordHash == null) {
        SendError(context, "username and passwordHash are required");
        return false;
      }

      try {
        if(lastFm == null) {
          lastFm = new LastFm();
          downloader.LastFm = lastFm;
        }
        lastFm.Initialize(username, passwordHash);
        initializedUsername = username;
        initializedPasswordHash = passwordHash;
        scrobbler.Username = username;
        scrobbler.PasswordHash = passwordHash;
        Trace.TraceInformation("Initialize completed for username {0}", username);
        Trace.Flush();
        return true;
      } catch(Exception ex) {
        Trace.WriteLine("Exception Initializing: " + ex);
        SendError(context, ex.Message);
      }
      return false;
    }


    private static void SendError(HttpListenerContext context, string message) {
      SendError(context, HttpStatusCode.BadRequest, message);
    }

    private static void SendError(HttpListenerContext context, HttpStatusCode httpStatusCode, string message) {
      Trace.TraceError("Responding with {0} and message {1} ", httpStatusCode, message);
      Trace.Flush();
      context.Response.StatusCode = (int)httpStatusCode;
      context.Response.StatusDescription = message;
      context.Response.Close();
    }
  }
}